Projeto: Bem-te-vi
Autor: Diego Abreu
Arquivo: passo_a_passo_bem-te-vi.ipynb
Resumo: Este arquivo tem por objetivo detalhar cada etapa do projeto Bem-te-vi.
Explicando cada uma das decisões tomadas e o código em linguagem Python utilizado durante os processos.


Passo a passo bem-te-vi

Etapas:

  • Instalação e importação de pacotes e bibliotecas;
  • Definição de chaves;
  • Coleta de dados;
  • Tratamento de dados;
  • Análise exploratória;
  • Apresentação de resultados;

Temos abaixo uma representação visual de como funciona o bem-te-vi:


Instalação e importação de pacotes e bibliotecas:

Este projeto necessita que dos pacotes e bibliotecas abaixo:

Tweepy: Biblioteca Open-source de fácil uso para acessar a API do Twitter. Disponível em: https://tweepy.readthedocs.io/en/latest/

JSON: Pacote nativo da linguagem python para manipulação de dados em formato JSON.

Unidecode: Pacote para retirada de caracteres especiais do texto, como emojis e acentos. Disponível em: https://pypi.org/project/Unidecode/#description

Its dangerous: Pacote para criptografia de dados. Disponível em: https://pythonhosted.org/itsdangerous/

Pandas: Pacote para manipulação de dados em formato dataframe. Disponível em: https://www.anaconda.com/distribution/

Plotly: Framework para criação de gráficos e dashboard. Disponível em: https://plot.ly/

MongoDB: Para armazenar os tweets coletados usaremos o mongoDB. Disponível em:https://www.mongodb.com/

PyMongo: Pacote para conexão com o mongoDB. Disponível em: https://pypi.org/project/pymongo/

Time: Pacote nativo da linguagem python para manipulação de dados em formato data/hora.

Date time: Biblioteca para manipulação de dados em formato data/hora. Disponível em: https://pypi.org/project/DateTime/

Scikit-learn: Biblioteca de aprendizado de máquina de código aberto para a linguagem de programação Python.

NLTK: Conjunto de bibliotecas para processamento de linguagem natural simbólica.

Emoji: Pacote para manipulação de emojis.

Vader: Ferramenta de análise de sentimentos baseada regras de vocabulário.

NBConvert: Pacote para exportar o Notebook em HTML.

In [1]:
# Instalações:
# !pip install tweepy
# !pip install Unidecode
# !pip install itsdangerous
# !pip install DateTime
# !pip install pymongo
# !pip install emoji
# !pip install vaderSentiment
# !pip install plotly==4.1.0
# nltk.download('stopwords')
# !pip install dash==1.3.0
# !pip install dash-daq==0.2.1
# !pip install selenium
# !pip install nbconvert
# Caso não possua um desses pacotes, descomente e rode essa célula.
In [2]:
# Importações:
# Tweepy:
import tweepy
from tweepy.streaming import StreamListener
from tweepy import OAuthHandler
from tweepy import Stream
# JSON:
import json
# Unidecode:
from unidecode import unidecode
# Its dangerous:
from itsdangerous import URLSafeSerializer
# Pandas 
import pandas as pd
# Plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# PyMongo
from pymongo import MongoClient 
# Time
import time
# Date Time
import datetime
# Sklearn
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn import metrics
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.feature_extraction.text import CountVectorizer
# NLTK
import nltk
from nltk.corpus import stopwords
# Emoji
import emoji
# Vader
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
# NBConvert
import nbconvert

Definição de chaves:

Para a coleta de dados no twitter é necessário ter uma conta de desenvolvedor na rede social. Após isso é necessário submeter uma aplicação para conseguir as 4 chaves de autenticação. A conta e as chaves devem ser obtidas pelo site: https://developer.twitter.com/ As 4 chaves são:

  • Consumer Key,
  • Consumer Secret,
  • Access Token;
  • Access Token Secret.
In [3]:
# Chaves da API do Twitter:
# Consumer Key
consumer_key = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Consumer Secret
consumer_secret = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Access Token
access_token = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"
# Access Token Secret
access_token_secret = "INSIRA_AQUI_SUA_CHAVE_DA_API_DO_TWITTER"

Usaremos o pacote itsdangerous para criptografar o campo com a identificação do usuário. Para essa transformação, é necessário definir uma chave que será usada como base dessa transformação. Pode ser uma simples palavra como 'panqueca' ou um termo mais complexo, fica a seu critério.
Essa mesma chave, caso necessário, poderá ser usada posteriormente para descriptografar o dado.

In [4]:
# Chave para criptografia:
cripto_key = 'INSIRA_AQUI_SUA_CHAVE_DE_CRIPTOGRAFIA'

É preciso definir quais são as palavras-chave queremos buscar. Tweets com essas que contenham essas palavras seram capturados pela nossa aplicação:

In [5]:
# Lista de palavras-chave para a coleta dos tweets:
keywords = ['INSIRA_AQUI_SUAS_PALAVRAS_CHAVE_PARA_BUSCA']
# Para mais de uma palavra faça:
# keywords = ['primeira', 'segunda','terceira', 'quarta', 'quinta']

Na sequência também é necessário que se defina qual será o nome da base de dados que receberá os dados coletas. Pode ser uma base que já exista ou um novo.

In [6]:
# Nome da base de dados no MongoDB:
banco = 'INSIRA_AQUI_O_NOME_DA_BASE_DE_DADOS'

Coleta de dados:

Criação das funções que serão utilizadas no processo de coleta:

In [7]:
# Autenticação com API do twitter:
auth = OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
# Função de criptografia:
cripto = URLSafeSerializer(cripto_key)

Conforme documentação do twitter disponível em: https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object . Definiremos na função de filtragem os campos que serão coletados. Os campos de cada tweet que coletaremos nesse projeto são:

  • _id: Identificação única para cada tweet publicado;
  • created_at: Data e hora da publicação do tweet;
  • screen_name: Identificação do usuário (@);
  • verified: Identifica se o usuário possui o selo de verificação da plataforma (ele é dado a personalidades, empresas e meios de comunicação);
  • location: Localização do usuário;
In [8]:
# Função de filtragem de tweets:
class Filtratweets(StreamListener):
    def on_data(self, dados):
        tweet = json.loads(dados)
        created_at = tweet["created_at"]
        cp_screen_name = cripto.dumps(tweet["user"]["screen_name"]) # Aplicamos aqui a função cripto para que o campo já entre camuflado no banco de dados.
        verified = tweet["user"]["verified"]
        text = tweet["text"]
        location = tweet["user"]["location"]
        
        obj = {"created_at":created_at, 
               "cp_screen_name": cp_screen_name,
               "verified":verified,
               "text":text,
               "location":location}
        
        tweetind = col.insert_one(obj).inserted_id
        print (obj)
        return True
    
    def on_error(self, status_code):
        if status_code == 420:
            return True

# Objeto filtragem de tweets
filtratweets = Filtratweets()
# Objeto captura de tweets, faz a conexão via API do Twitter
capturatweets = Stream(auth, listener = filtratweets, wait_on_rate_limit=True)

O próximo passo é estabelecer a conexão com o banco de dados, criar o banco e a coleção.

In [9]:
# Criação da conexão ao MongoDB
client = MongoClient('localhost', 27017)
# Criação do banco de dados
db = client[banco]
# Criação da collection
col = db[banco]

O Tweepy nos garante certa robustez na captura de dados, mas dependendo dos termos buscados o retorno pode trazer um alto volume de tweets em um curto período. O que pode causar erro e a interrupção da nossa aplicação, sendo necessário iniciá-la novamente. Para evitarmos isso, colocaremos a captura dentro de uma função que inicia a captura normalmente, mas que em caso de erro, pausa a coleta durante um determinado tempo (no caso, 10 segundos),e após esse período ele inicia a retoma novamente.

In [10]:
# Função que inicia a coleta de tweets:
def inicia_coleta():
    while True:
        try:
            capturatweets.filter(languages=["pt"], track=keywords) # O parâmetro language define que capturaremos tweets em português
        except:
            time.sleep(10) # Define o tempo que a aplicação deve aguardar para rodar novavemente em caso de erros.
            continue
In [ ]:
# Inicia a coleta dos tweets
inicia_coleta()
In [ ]:
# --> Pressione o botão Stop na barra de ferramentas duas vezes para encerrar a captura dos Tweets
In [ ]:
# Finaliza a conexão com a API do Twitter
capturatweets.disconnect()

Tratamento de dados

Nessa etapa, primeiramente, vamos acessar os dados no banco de dados e visualizá-los utilizando o pacote pandas.

In [11]:
# criação de um dataset com dados retornados do MongoDB
dataset = [{"created_at": item["created_at"], 
            "cp_screen_name": item["cp_screen_name"], 
            "verified": item["verified"],
            "text": item["text"],
            "location": item["location"],
           } for item in col.find()]
# Criação do dataframe
df = pd.DataFrame(dataset)

Caso tenha feito a coleta em outro momento ou queira uar um dataset já existente. basta descomentar e rodar a célula abaixo.
O único requisito para que o código funcione bem, é que o dataset tenha as variáveis (colunas) com os mesmos nomes: created_at, cp_screen_name, verified, text e location.

In [12]:
# Código para aplicar a análise em outro dataset, descomente e rode.
# O arquivo da coleta tweets_GRExFLA.csv está disponível no diretório do projeto:
# df = pd.read_csv('dados_tweets_coletados/tweets_GRExFLA.csv')

Nesse arquivo de passo a passo, faremos a análise de tweets coletados durante o jogo de futebol Grêmio X Flamengo pela Copa Libertadores da América que ocorreu no dia 02/10/2019.
O objetivo era observar o comportamento da torcida rubro-negra. Por isso as palavras-chave utilizada na busca foram:
'flamengo', 'Flamengo', '#CRF', 'Mengo' e 'mengo'.
A API do Tweepy é case-sensitive, ou seja, faz diferenciação entre maiúsculas e minúsculas. Por isso, as palavras-chave forma escritas de diferentes formas.
Vamos partir agora para nossa análise:

Total de registros em nosso dataset:

In [13]:
total_tweets = len(df)
total_tweets
Out[13]:
180114

Visualização dos 5 primeiros tweets coletados:

In [14]:
df.head()
Out[14]:
cp_screen_name created_at location text verified
0 IkNoZWxseXBob3RvMSI.Vss61si8Uu8ExHngtTvkA1ziAHg Wed Oct 02 23:59:56 +0000 2019 Acari, Rio de Janeiro O show vai começar, pra cima deles Mengão.\n#f... False
1 IkpGXzIyMSI._eqKMQ8J8guslo81ZQVpCYI2lyI Wed Oct 02 23:59:56 +0000 2019 Nova Iguaçu, Brasil RT @paolo_crf: Ainda bem q o filho de Deus é t... False
2 Ikd1aVNvdXphMTg5NSI.9e1Lmo13f9dteiU0qJA3Wx2KwZk Wed Oct 02 23:59:56 +0000 2019 Rio Grande do Norte, Brasil RT @Flamengo: Tudo pronto! #JogaremosJuntos #G... False
3 ImxpbmlrZWZlcnJldHRpIg.akeM9KB49XS5GADlXI5DZXI... Wed Oct 02 23:59:57 +0000 2019 Mombaça, Brasil RT @Flamengo: Aquecimento rolando! #JogaremosJ... False
4 Im1hdGhldXNjcmF2bzIyMSI.JQFEyo4vB7KKncjQrFUTzO... Wed Oct 02 23:59:57 +0000 2019 None RT @JrrPeixoto: Toda hora eu fico arrepiado pe... False

As células abaixo configura o ambiente para mostrar os dados e processos de forma mais agradavél.

In [15]:
# Ajuste de visualização:
pd.set_option('display.max_colwidth', -1)
pd.set_option('display.max_columns', 100)
pd.set_option('display.max_rows', 100)

# Não mostrar avisos e alertas sobre versões desatualizadas, processos e etc. 
import sys
if not sys.warnoptions:
    import warnings
    warnings.simplefilter("ignore")
In [16]:
df.head()
Out[16]:
cp_screen_name created_at location text verified
0 IkNoZWxseXBob3RvMSI.Vss61si8Uu8ExHngtTvkA1ziAHg Wed Oct 02 23:59:56 +0000 2019 Acari, Rio de Janeiro O show vai começar, pra cima deles Mengão.\n#flamengo\n#Gabigoooool\n#arrascaeta\n#brunohenrique https://t.co/dsaV4XrpEY False
1 IkpGXzIyMSI._eqKMQ8J8guslo81ZQVpCYI2lyI Wed Oct 02 23:59:56 +0000 2019 Nova Iguaçu, Brasil RT @paolo_crf: Ainda bem q o filho de Deus é técnico do Flamengo... https://t.co/GcA4AvEkYv False
2 Ikd1aVNvdXphMTg5NSI.9e1Lmo13f9dteiU0qJA3Wx2KwZk Wed Oct 02 23:59:56 +0000 2019 Rio Grande do Norte, Brasil RT @Flamengo: Tudo pronto! #JogaremosJuntos #GRExFLA https://t.co/sitpmFggXx False
3 ImxpbmlrZWZlcnJldHRpIg.akeM9KB49XS5GADlXI5DZXIrbxg Wed Oct 02 23:59:57 +0000 2019 Mombaça, Brasil RT @Flamengo: Aquecimento rolando! #JogaremosJuntos #GRExFLA https://t.co/nnMNm2dOo8 False
4 Im1hdGhldXNjcmF2bzIyMSI.JQFEyo4vB7KKncjQrFUTzOlOVYI Wed Oct 02 23:59:57 +0000 2019 None RT @JrrPeixoto: Toda hora eu fico arrepiado pensando no jogo do Flamengo. Muita tensão. Fico imaginando o Flamengo na final e começo a chor… False

Felizmente os dados já vem com um boa estrutura, praticamente pronto para iniciar a análise exploraória. Portanto, inicialmente faremos apenas o tratamento na variável de data/hora (created_at), pois ele não vem em nosso fuso horário. Os demais tratamento serão feitos a medida em que forem necessário na fase exploratória.

In [17]:
# Tratamento da váriavel created_at:
# Função para formatar data/hora:
def format_datetime(dt_series):
    def get_split_date(strdt):
        split_date = strdt.split()
        str_date = split_date[1] + ' ' + split_date[2] + ' ' + split_date[5] + ' ' + split_date[3]+ ' ' + split_date[4]
        return str_date
    dt_series = pd.to_datetime(dt_series.apply(lambda x: get_split_date(x)), format = '%b %d %Y %H:%M:%S %z')
    return dt_series

# Cria nova variável para o horário correto:
df['data_hora'] = format_datetime(df['created_at'])
# Converte para o nosso fuso horário:
df['data_hora'] = df['data_hora'].dt.tz_convert('America/Sao_Paulo')

Visualização dos 5 primeiros tweets coletados, após o tratamento de data e hora:

In [18]:
df.head()
Out[18]:
cp_screen_name created_at location text verified data_hora
0 IkNoZWxseXBob3RvMSI.Vss61si8Uu8ExHngtTvkA1ziAHg Wed Oct 02 23:59:56 +0000 2019 Acari, Rio de Janeiro O show vai começar, pra cima deles Mengão.\n#flamengo\n#Gabigoooool\n#arrascaeta\n#brunohenrique https://t.co/dsaV4XrpEY False 2019-10-02 20:59:56-03:00
1 IkpGXzIyMSI._eqKMQ8J8guslo81ZQVpCYI2lyI Wed Oct 02 23:59:56 +0000 2019 Nova Iguaçu, Brasil RT @paolo_crf: Ainda bem q o filho de Deus é técnico do Flamengo... https://t.co/GcA4AvEkYv False 2019-10-02 20:59:56-03:00
2 Ikd1aVNvdXphMTg5NSI.9e1Lmo13f9dteiU0qJA3Wx2KwZk Wed Oct 02 23:59:56 +0000 2019 Rio Grande do Norte, Brasil RT @Flamengo: Tudo pronto! #JogaremosJuntos #GRExFLA https://t.co/sitpmFggXx False 2019-10-02 20:59:56-03:00
3 ImxpbmlrZWZlcnJldHRpIg.akeM9KB49XS5GADlXI5DZXIrbxg Wed Oct 02 23:59:57 +0000 2019 Mombaça, Brasil RT @Flamengo: Aquecimento rolando! #JogaremosJuntos #GRExFLA https://t.co/nnMNm2dOo8 False 2019-10-02 20:59:57-03:00
4 Im1hdGhldXNjcmF2bzIyMSI.JQFEyo4vB7KKncjQrFUTzOlOVYI Wed Oct 02 23:59:57 +0000 2019 None RT @JrrPeixoto: Toda hora eu fico arrepiado pensando no jogo do Flamengo. Muita tensão. Fico imaginando o Flamengo na final e começo a chor… False 2019-10-02 20:59:57-03:00

Caso queira, é possível exportar o dataset no estado descomentando e rodando o código abaixo:

In [19]:
# Exporta a coleta em arquivo .csv , descomente e rode:
#df.to_csv('coleta_de_tweets.csv', index=False)

Análise exploratória:

Para nossa análise exploratória definimos algumas questões para serem respondidas:

  • Quantidade de tweets;
  • Quantidade de usuários únicos;
  • Quantidade de tweets ao longo do período de coleta;
  • Análise e classificação do tweet como positivo, negativo ou neutro;
  • Top 5 hashtags usadas;
  • Top 5 palavras mais presentes;
  • Top 5 estados com mais tweets;
  • Taxas de tweets por usuário, por minuto e por segundo;

Nessa etapa utilizaremos o pacote pandas para as análises quantitativas, os algoritmos Multinomial Naive Bayes e Vader para a análise de sentimentos e o Plotly para a criação de gráficos.

Quantidade de tweets:

Na etapa anterior verificamos a quantidade de registros. Esse valor é a nossa quantidade de tweets coletados. Porém podemos enriquecer essa informação identificando quais deles são originais e quais deles são retweets(tweets que foram replicados por outros usuários). Os retweets tem como característica ter a sigal "RT" antes do conteúdo. Portanto vamos identicar em quantos registros a variável "text" começa com "RT".

In [20]:
# Quantidade total de tweets:
total_tweets = len(df)
# Cria uma variavél chamada "retweeted" que armazena se o conteúdo do tweet começa com RT ou não:
df['retweeted'] = df['text'].str.startswith('RT')
# Contagem dos retweets:
retweets = len(df[df['retweeted'] == True])
# Contagem dos originais:
originais = len(df[df['retweeted'] == False])

# Exibindo as contagens:
print("Quantidade de tweets originais: ", originais)
print("Quantidade de retweets: ", retweets)
print("Quantidade total dos tweets: ", total_tweets)
Quantidade de tweets originais:  81214
Quantidade de retweets:  98900
Quantidade total dos tweets:  180114
In [21]:
# Gráfico da quantidade total de tweets:
total_tt_st = str(total_tweets) + "  Tweets"
fig = go.Figure(data = [go.Pie(labels = ['Originais','Retweets'],values = [originais, retweets], 
                               hole = .5, marker_colors = ['gold', 'goldenrod'],
                               textinfo = 'label+percent', hoverinfo = 'value')])

fig.update_layout(template="plotly_dark",
                  title = go.layout.Title(text = total_tt_st,xref = "paper", x=0.5))
fig.show()

Quantidade de usuários únicos:

Apesar de criptografados, podemos fazer a contagem do número de usuários. Pois o algoritmo de criptografia segue um padrão baseado pela palavra chave, então palavras iguais possuem correspondentes criptogrados iguais. Portanto basta apenas fazer uma contagem simples. Assim como o tópico anterior, também podemos enriquecer a análise. Pois através da variavél "verified" podemos identificar quantos usuários possuem o selo de verificação do twitter.

In [22]:
# Contagem de usuários únicos:
qtd_usuarios_unicos = df['cp_screen_name'].nunique()
# Contagem de usuários verificados:
usuarios_verificados = df[df['verified'] == True]
total_tt_verificados = len(usuarios_verificados)
qtd_usuarios_verificados = usuarios_verificados['cp_screen_name'].nunique()
# Contagem de usuários não verificados:
usuarios_nao_verificados = df[df['verified'] == False]
total_tt_nao_verificados = len(usuarios_nao_verificados)
qtd_usuarios_nao_verificados = usuarios_nao_verificados['cp_screen_name'].nunique()

# Exibindo as contagens:
print("Quantidade de usuários verificados: ", qtd_usuarios_verificados)
print("Quantidade de usuários não verificados: ", qtd_usuarios_nao_verificados)
print("Quantidade total de usuários: ", qtd_usuarios_unicos)
Quantidade de usuários verificados:  192
Quantidade de usuários não verificados:  98527
Quantidade total de usuários:  98719
In [23]:
# Gráfico da quantidade total de usuários:
total_us_st = str(qtd_usuarios_unicos) + " Usuários"
fig = go.Figure(data = [go.Pie(labels = ['Verificados', 'Não verificados'],
                    values = [qtd_usuarios_verificados, qtd_usuarios_nao_verificados],
                    hole = .5, marker_colors =['darkgoldenrod','gold'],
                    textinfo = 'label+percent', hoverinfo = 'value')])

fig.update_layout(template="plotly_dark",
                  title = go.layout.Title(text = total_us_st, xref = "paper", x=0.5))
fig.show()

Quantidade de tweets ao longo do período de coleta:

Para obtermos essa informação precisamos contar quantos tweets foram publicados no mesmo horário.

In [24]:
# Cria um dataframe com a quantidade de tweets por horário:
tw_x_pd = df['data_hora'].value_counts().to_frame().reset_index()
tw_x_pd.columns = ['data_hora', 'qtd_tweets']
# Ordenar pelo horário:
tw_x_pd = tw_x_pd.sort_values(by=['data_hora'])

# Exibe os 5 primeiros horários:
tw_x_pd.head()
Out[24]:
data_hora qtd_tweets
7539 2019-10-02 20:59:56-03:00 3
6652 2019-10-02 20:59:57-03:00 11
6921 2019-10-02 20:59:58-03:00 10
5889 2019-10-02 20:59:59-03:00 14
4126 2019-10-02 21:00:00-03:00 20
In [25]:
# Gráfico da quantidade ao longo do período:
fig = go.Figure(data = [go.Scatter(x = tw_x_pd['data_hora'], y = tw_x_pd['qtd_tweets'], 
                                   fill = 'tozeroy', mode = 'lines', line = dict(color = 'gold', width = 0.5))])
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Tweets ao longo do período",xref = "paper", x = 0.5))
fig.show()

Análise e classificação do tweet como positivo, negativo ou neutro:

Do nosso projeto essa talvez seja a com maior grau de complexidade. Para a análise de sentimentos usaremos dois algoritmos o Multinomial Naive Bayes e Vader.

Há diversos algoritmos de análise de sentimentos disponíveis com bom desempenho, o Vader é um exemplo, porém eles não fazem essa análise em língua portuguesa. Devido a grande quantidade de tweets em algumas análises, a tradução do tweets para inglês para depois passarem por um desses algoritmos, se mostrou demorado e em alguns casos apresentou erros na API de tradução devido ao alto número de traduções.

Sendo descartada a opção de utilizar um algoritmo pronto, a alternativa para nosso projeto foi criar um algoritmo utilizando Multinomial NB treinado com um dataset obtido no Kaggle. Link para o dataset:https://www.kaggle.com/augustop/portuguese-tweets-for-sentiment-analysis/ .

Os arquivos usados foram os Test3classes.csv e Train3Classes.csv. O dataset de treino possui 100 mil tweets em português já classificados em Negativo, Positivo e Neutro. O de teste possui 4999 registros classificados.

Para facilitar nosso processo, baseado nesses dois arquivos foram criados os arquivos df_treino.csv e df_teste.csv. Eles teem o mesmo conteúdo dos arquivos do Kaggle porém com alguns ajustes: exclusão de colunas desnecessárias, e transformação dos valores da variável "sentiment" de 0,1,2 para Negativo, Positivo e Neutro. Esses arquivos estão disponíveis no diretório desse projeto.

In [26]:
# Criação do Algoritmo de análise de sentimentos:
# Leitura dos dados de treino: 
df_treino = pd.read_csv("dados_base_analise_de_sentimentos/df_treino.csv")
# Importação de stop words em português do pacote NLTK:
portugues_stops = set(stopwords.words('portuguese'))
# Adição de novas stop words identificadas como faltantes em análises anteriores:
novas_stopwords = ['tá', 'ta', 'pra','pro']
portugues_stops = portugues_stops.union(novas_stopwords)
# Função de tratamento do texto:
tf_vectorizer = TfidfVectorizer(stop_words = portugues_stops,analyzer='word', ngram_range=(1, 1),
                                lowercase=True, use_idf=True, strip_accents='unicode')
# Definição de variáveis de treinamento e aplicação da função de tratamento de texto:
treino_x = tf_vectorizer.fit_transform(df_treino['tweets'])
treino_y = df_treino['sentimento']
# Criação do modelo de análise:
classificador_MNB = MultinomialNB()
# Treinamento do modelo:
classificador_MNB.fit(treino_x, treino_y)
Out[26]:
MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)

Teste do modelo:

In [27]:
# Leitura dos dados de teste: 
df_teste = pd.read_csv("dados_base_analise_de_sentimentos/df_teste.csv")
# Definição de variáveis de teste e aplicação da função de tratamento de texto:
teste_x =  tf_vectorizer.transform(df_teste['tweets'])
teste_y = df_teste['sentimento']
# Aplicação do modelo nos dados de teste:
predicao_MNB_teste = classificador_MNB.predict(teste_x)
resultado_MNB_teste = pd.Series(predicao_MNB_teste)
# Resultados da análise nos dados de teste:
resultado_MNB_teste.value_counts()
Out[27]:
Neutro      1765
Negativo    1666
Positivo    1568
dtype: int64

Avaliando a precisão do modelo criado:

In [28]:
# Matrix de confusão:
print (confusion_matrix(teste_y,resultado_MNB_teste))
[[1198   53  415]
 [  10 1628   28]
 [ 458   84 1125]]
In [29]:
# Verificação de métricas de precisão:
print (classification_report(teste_y,resultado_MNB_teste))
              precision    recall  f1-score   support

    Negativo       0.72      0.72      0.72      1666
      Neutro       0.92      0.98      0.95      1666
    Positivo       0.72      0.67      0.70      1667

   micro avg       0.79      0.79      0.79      4999
   macro avg       0.79      0.79      0.79      4999
weighted avg       0.79      0.79      0.79      4999

In [30]:
# Taxa de acurácia do modelo:
metrics.accuracy_score(teste_y,resultado_MNB_teste)
Out[30]:
0.7903580716143228

Nosso modelo apresentou uma taxa de 79% de acerto. Como nosso projeto faz análises de forma genérica e foi treinado com tweets também genéricos, essa taxa pode ser considerada um bom valor. Para melhora-lá pode-se treinar o modelo com tweets mais similares aos que ele irá analisar. Por exemplo, Se o objetivo é analisar tweets sobre cinema, treinar com tweets sobre cinema.

Vamos agora aplicar nosso modelo de predição de sentimentos nos tweets coletados.

In [31]:
# Aplicação da função de tratamento de texto:
analise_sent_tweets = tf_vectorizer.transform(df['text'])
# Aplicação do modelo
predicao_MNB = pd.Series(classificador_MNB.predict(analise_sent_tweets))
# Armazenamento dos resultados em uma nova variável chamada "analise_sentimento":
df['analise_sentimento'] = predicao_MNB

# Visualização do dataset após a análise de sentimentos:
df.head()
Out[31]:
cp_screen_name created_at location text verified data_hora retweeted analise_sentimento
0 IkNoZWxseXBob3RvMSI.Vss61si8Uu8ExHngtTvkA1ziAHg Wed Oct 02 23:59:56 +0000 2019 Acari, Rio de Janeiro O show vai começar, pra cima deles Mengão.\n#flamengo\n#Gabigoooool\n#arrascaeta\n#brunohenrique https://t.co/dsaV4XrpEY False 2019-10-02 20:59:56-03:00 False Positivo
1 IkpGXzIyMSI._eqKMQ8J8guslo81ZQVpCYI2lyI Wed Oct 02 23:59:56 +0000 2019 Nova Iguaçu, Brasil RT @paolo_crf: Ainda bem q o filho de Deus é técnico do Flamengo... https://t.co/GcA4AvEkYv False 2019-10-02 20:59:56-03:00 True Neutro
2 Ikd1aVNvdXphMTg5NSI.9e1Lmo13f9dteiU0qJA3Wx2KwZk Wed Oct 02 23:59:56 +0000 2019 Rio Grande do Norte, Brasil RT @Flamengo: Tudo pronto! #JogaremosJuntos #GRExFLA https://t.co/sitpmFggXx False 2019-10-02 20:59:56-03:00 True Positivo
3 ImxpbmlrZWZlcnJldHRpIg.akeM9KB49XS5GADlXI5DZXIrbxg Wed Oct 02 23:59:57 +0000 2019 Mombaça, Brasil RT @Flamengo: Aquecimento rolando! #JogaremosJuntos #GRExFLA https://t.co/nnMNm2dOo8 False 2019-10-02 20:59:57-03:00 True Neutro
4 Im1hdGhldXNjcmF2bzIyMSI.JQFEyo4vB7KKncjQrFUTzOlOVYI Wed Oct 02 23:59:57 +0000 2019 None RT @JrrPeixoto: Toda hora eu fico arrepiado pensando no jogo do Flamengo. Muita tensão. Fico imaginando o Flamengo na final e começo a chor… False 2019-10-02 20:59:57-03:00 True Negativo

Foi mencionado anteriormente que também usaríamos o algoritmo Vader. Inicialmente ele foi descartado por trabalharmos com tweets em português. Porém durante as análises surgiu uma oportunidade para o utilizarmos e melhorar nossas predições.

O modelo Multinomial não trabalha com emojis, e durante o tratamento do texto eles são removidos. Entretanto, os emojis são cada vez mais usados e podem fazer total diferença no sentimento do conteúdo.

In [32]:
# frases com sentimentos Neutro, Positivo e Negativo
frases_para_teste = pd.Series(['Estou indo comprar um computador novo.',
                               'Estou indo comprar um computador novo. 😃',
                               'Estou indo comprar um computador novo. 😠'])
In [33]:
# Análise com modelo multinomialNB
resultado_teste_MNB = pd.Series(classificador_MNB.predict(tf_vectorizer.transform(frases_para_teste)))
resultado_teste_MNB
Out[33]:
0    Positivo
1    Positivo
2    Positivo
dtype: object

Podemos considerar que nosso modelo fez uma boa classificação, apesar da primeira frase possuir uma interpretação dúbia. Porém na última o emoji dar claramente uma conotação negativa a frase, e ela acaba não sendo captada.

O algoritmo Vader trabalha muito bem com emojis como veremos a seguir. Note que a primeira ele irá classificar como neutro por estar em um idioma em que ele não foi treinado.

In [34]:
# Criação do modelo Vader:
analyser = SentimentIntensityAnalyzer()
def vader(text):
    score = analyser.polarity_scores(text)
    lb = score['compound']
    if lb >= 0.05:
        return 'Positivo'
    elif (lb > -0.05) and (lb < 0.05):
        return 'Neutro'
    else:
        return 'Negativo'
In [35]:
resultado_teste_Vader = frases_para_teste.apply(lambda x: vader(x))
resultado_teste_Vader
Out[35]:
0    Neutro  
1    Positivo
2    Negativo
dtype: object

Outra amostra de como o Vader consegue fazer cálculos com emojis e determinar se eles são positivos ou não:

In [36]:
# Novo teste com Vader:
frases_para_teste_2 = pd.Series(['😄', '👍 👎', '😐 😜 😁', '🧐 😠'])

resultado_teste_Vader = frases_para_teste_2.apply(lambda x: vader(x))
resultado_teste_Vader
Out[36]:
0    Positivo
1    Neutro  
2    Positivo
3    Negativo
dtype: object

Como os emojis tem sido cada vez mais usados, não podemos deixar de incluí-los em nossa análise. Por isso, vamos verificar quais são os tweets com emojis e extraí-los para uma nova variável, e classificá-los com o modelo Vader.

Como os emojis possuem um peso maior em relação a sentimentos do que as palavras, aqueles tweets em que o resultado do Vader for positivo ou negativo, a classificação será a do modelo Vader.

Nos casos onde os tweets não tenha emojis, ou tenha e o resultado do Vader seja neutro, será mantida a classificação feita pelo modelo MultinomialNB.

In [37]:
# Análise de emojis:
# Função de extração:
def extracao_de_emojis(str):
  return ' '.join(c for c in str if c in emoji.UNICODE_EMOJI)
# Criação da nova variável contendo os emojis:
df['emojis_extraidos'] = df['text'].apply(lambda x: extracao_de_emojis(x))
# Aplicação do modelo Vader
df['analise_sent_emoji'] = df['emojis_extraidos'].apply(lambda x: vader(x))
# Substituição das análises dos tweets com emojis
df.loc[df['analise_sent_emoji'] == 'Positivo', 'analise_sentimento'] = 'Positivo'
df.loc[df['analise_sent_emoji'] == 'Negativo', 'analise_sentimento'] = 'Negativo'

# Visualização do dataset após a análise de sentimentos:
df.head()
Out[37]:
cp_screen_name created_at location text verified data_hora retweeted analise_sentimento emojis_extraidos analise_sent_emoji
0 IkNoZWxseXBob3RvMSI.Vss61si8Uu8ExHngtTvkA1ziAHg Wed Oct 02 23:59:56 +0000 2019 Acari, Rio de Janeiro O show vai começar, pra cima deles Mengão.\n#flamengo\n#Gabigoooool\n#arrascaeta\n#brunohenrique https://t.co/dsaV4XrpEY False 2019-10-02 20:59:56-03:00 False Positivo Neutro
1 IkpGXzIyMSI._eqKMQ8J8guslo81ZQVpCYI2lyI Wed Oct 02 23:59:56 +0000 2019 Nova Iguaçu, Brasil RT @paolo_crf: Ainda bem q o filho de Deus é técnico do Flamengo... https://t.co/GcA4AvEkYv False 2019-10-02 20:59:56-03:00 True Neutro Neutro
2 Ikd1aVNvdXphMTg5NSI.9e1Lmo13f9dteiU0qJA3Wx2KwZk Wed Oct 02 23:59:56 +0000 2019 Rio Grande do Norte, Brasil RT @Flamengo: Tudo pronto! #JogaremosJuntos #GRExFLA https://t.co/sitpmFggXx False 2019-10-02 20:59:56-03:00 True Positivo Neutro
3 ImxpbmlrZWZlcnJldHRpIg.akeM9KB49XS5GADlXI5DZXIrbxg Wed Oct 02 23:59:57 +0000 2019 Mombaça, Brasil RT @Flamengo: Aquecimento rolando! #JogaremosJuntos #GRExFLA https://t.co/nnMNm2dOo8 False 2019-10-02 20:59:57-03:00 True Neutro Neutro
4 Im1hdGhldXNjcmF2bzIyMSI.JQFEyo4vB7KKncjQrFUTzOlOVYI Wed Oct 02 23:59:57 +0000 2019 None RT @JrrPeixoto: Toda hora eu fico arrepiado pensando no jogo do Flamengo. Muita tensão. Fico imaginando o Flamengo na final e começo a chor… False 2019-10-02 20:59:57-03:00 True Negativo Neutro

Com nossa análise de sentimentos devidamente concluída podemos agora quantificá-la e produzir gráficos.

In [38]:
# Contagem de sentimentos
polaridade_sentimentos = pd.DataFrame(df['analise_sentimento'].value_counts()).reset_index()
polaridade_sentimentos.columns = ['sentimento', 'num_de_tweets']
polaridade_sentimentos
Out[38]:
sentimento num_de_tweets
0 Positivo 94882
1 Neutro 61368
2 Negativo 23864
In [39]:
# Gráfico com a contagem de polaridade
cores_dic = {'Positivo': 'gold', 'Neutro': 'white', 'Negativo': 'darkgoldenrod'}
cores= polaridade_sentimentos['sentimento'].map(cores_dic)
fig = go.Figure(data = [go.Bar( x = polaridade_sentimentos['num_de_tweets'],
                                y = polaridade_sentimentos['sentimento'], orientation = 'h', 
                                marker_color = cores)
                                ]).update_yaxes(categoryorder = "total ascending")
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Contagem de polaridade",xref = "paper", x = 0.5))
fig.show()
In [40]:
# Contagem de sentimentos por horário:
# Criação de um dataframe para essa  análise:
total_sents = pd.DataFrame()
total_sents['horario'] = df['data_hora']
total_sents['sentimento'] = df['analise_sentimento']
# Contagem dos tweets positivos:
total_sents_pos = total_sents[total_sents['sentimento'] == 'Positivo']
total_sents_pos = total_sents_pos['horario'].value_counts().to_frame().reset_index()
total_sents_pos.columns = ['data_hora', 'qtd_tweets']
total_sents_pos = total_sents_pos.sort_values(by=['data_hora'])
# Contagem dos tweets neutros:
total_sents_neu = total_sents[total_sents['sentimento'] == 'Neutro']
total_sents_neu = total_sents_neu['horario'].value_counts().to_frame().reset_index()
total_sents_neu.columns = ['data_hora', 'qtd_tweets']
total_sents_neu = total_sents_neu.sort_values(by=['data_hora'])
# Contagem dos tweets negativos:
total_sents_neg = total_sents[total_sents['sentimento'] == 'Negativo']
total_sents_neg = total_sents_neg['horario'].value_counts().to_frame().reset_index()
total_sents_neg.columns = ['data_hora', 'qtd_tweets']
total_sents_neg = total_sents_neg.sort_values(by=['data_hora'])
In [41]:
# Gráfico com a contagem de sentimentos por horário:
fig = go.Figure()
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"))
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"))
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"))
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Contagem de sentimentos por horário",xref = "paper", x = 0.5))
fig.show()
In [42]:
# Gráfico com a contagem de sentimentos por horário detalhado:
fig = make_subplots(
    rows=3, cols=1, start_cell="top-left",
    specs=[[{}],[{}],[{}]],
    subplot_titles = ("Positivo", "Neutro", "Negativo"))


#fig = go.Figure()
# Positivo:
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"), row = 1,col = 1)
# Neutro:
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"), row = 2 ,col = 1)
# Negativo:
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"), row = 3,col = 1)
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Contagem de sentimentos por horário",xref = "paper", x = 0.5))
fig.show()

Top 5 hashtags usadas:

Esta análise é em teoria similar à análise de retweets, mas com a diferença de que os "RT's" sempre estão presentes no início do conteúdo, já as hashtags podem estar em qualquer posição dentro da variável "text". Por isso requer alguns tratamentos adicionais, que também servirão para adiantar o trabalho na questão de top 5 palavras.

In [43]:
# Criação do dataframe para a análise:
verifica_hash = pd.DataFrame()
verifica_hash['text'] = df['text']
# Substituição dos caracteres # e @ antes do tratamento para que não sejam excluídos no processo:
verifica_hash['text'] = verifica_hash['text'].str.replace('#', 'hashtag_vl', regex=True)
verifica_hash['text'] = verifica_hash['text'].str.replace('@', 'arroba_vl', regex=True)
# Utilização do método CountVectorizer para criar uma matriz de documentos:
cv = CountVectorizer(strip_accents = None)
count_matrix = cv.fit_transform(verifica_hash['text'])
# Criação de um dataframe com o número de ocorrências das principais palavras em nosso dataset:
contagem_palavras = pd.DataFrame(cv.get_feature_names(), columns=["palavra"])
contagem_palavras["count"] = count_matrix.sum(axis=0).tolist()[0]
contagem_palavras = contagem_palavras.sort_values("count", ascending=False).reset_index(drop=True)
# Retorno do caracteres # e @:
contagem_palavras['palavra'] = contagem_palavras['palavra'].str.replace('hashtag_vl','#', regex=True)
contagem_palavras['palavra'] = contagem_palavras['palavra'].str.replace('arroba_vl','@', regex=True)
In [44]:
# Contagem de Hashtags:
hashtags = contagem_palavras[contagem_palavras['palavra'].str.startswith('#')]
# Separação e exibição das 5 mais presentes:
top5_hashtags = hashtags.head()
top5_hashtags
Out[44]:
palavra count
18 #grexfla 15329
29 #flamengo 9880
30 #jogaremosjuntos 9811
184 #grêmio 1691
210 #libertadores 1482
In [45]:
# Gráfico de contagem de hashtags:
fig = go.Figure(data = [go.Bar(y = top5_hashtags['count'], x = top5_hashtags['palavra'],
                     marker_color='gold')])
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Top 5 hashtags #",xref = "paper", x = 0.5))
fig.show()

Top 5 palavras mais presentes:

Nesse tópico, vamos aproveitar o dataframe "contagem_palavras" criado durante a etapa anterior e refiná-lo com alguns tratamentos que incluem: remoção de hashtags, nomes de usuários, termos irrelevantes e stop words.

In [46]:
# Arrobas de usuários citadas nos tweets:
arrobas_citadas = contagem_palavras[contagem_palavras['palavra'].str.startswith('@')]
# Remoção hashtags e arrobas
contagem_palavras_relevantes = contagem_palavras[(contagem_palavras['palavra'].isin(hashtags['palavra'])==False)]
contagem_palavras_relevantes = contagem_palavras_relevantes[(contagem_palavras_relevantes['palavra'].isin(arrobas_citadas['palavra'])==False)]
# Remoção de termos irrelevantes:
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'rt']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'https']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'http']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != 'co']
contagem_palavras_relevantes = contagem_palavras_relevantes[contagem_palavras_relevantes['palavra'] != '#']
# Remoção de stop words:
contagem_palavras_relevantes = contagem_palavras_relevantes[(contagem_palavras_relevantes['palavra'].isin(portugues_stops)==False)]
# Separação e exibição das 5 mais presentes:
top5_contagem_palavras_relevantes = contagem_palavras_relevantes.head()
top5_contagem_palavras_relevantes
Out[46]:
palavra count
0 flamengo 142322
8 grêmio 24717
13 vai 18844
15 jogo 18600
16 gol 16989
In [47]:
# Gráfico de contagem de palavras relevantes:
fig = go.Figure(data = [go.Bar(x = top5_contagem_palavras_relevantes['palavra'],
                               y = top5_contagem_palavras_relevantes['count'], 
                               marker_color='gold')])
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Top 5 palavras",xref = "paper", x = 0.5))
fig.show()

Top 5 estados com mais tweets:

A localização de um tweet pode ser obtida por duas variáveis a "location" e a "place". Com base em coletas anteriores, foi possível verificar que a "place" apresenta geralmente valor nulo. Isso porque é uma variável que só é preenchida quando o usuário opta por compartilhar sua localização exata no tweet, como em uma postagem de "check-in" em um restaurante, por exemplo. Esse recurso é pouco utilizado pelos usuários da rede, por isso nem coletamos esse dado.

Já a "location" é uma informação que consta na bio do usuário, não é precisa como a place, mas nos dá uma localização a nivel de cidade/estado/país. Entretanto, essa variável também tem seus problemas. Ela é um campo de texto livre, sem formato definido. Sendo assim os usuários escrevem de forma variada, só o país, país e cidade, estado e cidade, lugares fictícios, trocadilhos e outras coisas. Como não é um campo obrigatório, também temos o problema de falta de preenchimento, mas esse em um número bem menor.

Porém entre os que preenchem de forma minimamente adequada, é possivel notar um certo padrão. Veja alguns exemplos de preenchimento:

  • Manaus, Brasil
  • Porto Alegre
  • Rio de Janeiro, Brasil
  • Madureira - RJ
  • Joinville, Santa Catarina - BR

Geralmente temos presentes nomes de cidades e estados. Então conseguimos garantir uma certa precisão em relação ao estado, visto que com o nome da cidade é possível saber o estado, já o inverso não. Para essa análise utilizaremos um dataset disponibilizado no Kaggle que contém uma lista com todas as cidades brasileiras. link do dataset:https://www.kaggle.com/gilbertotrindade/cidades-brasileiras.

Assim como foi feito para análise de sentimentos, utilizei este dataset para criar um dicionário de dados em python (arquivo dicionario_brasil.py, disponível no projeto). Nele temos as cidades e estados e suas respectivas siglas (UF). A missão é mapear todas as localizações e substituí-las pelas siglas dos estados.

Geralmente ao final da análise é possível ter a localização estadual de um número signifcativos de tweets. Mas de acordo com os dados coletados esse número pode variar a ponto de não gerar informação relevante quando comparada ao todo.

In [48]:
# Leitura do arquivo que contém as cidades e estados brasileiros.
import dicionario_brasil 
## Tratamento dos dados:
df['localizacao'] = df['location']
df['localizacao'] = df['localizacao'].str.upper()
df['localizacao'] = df['localizacao'].fillna('NULO')
df['localizacao'] = df['localizacao'].apply(unidecode)
df['localizacao'] = df['localizacao'].str.replace('BRASIL', 'NULO', regex=True)
df['localizacao'] = df['localizacao'].str.replace('-', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace('/', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace('|', ',', regex=True)
df['localizacao'] = df['localizacao'].str.replace(' ', '', regex=True)
# Divisão dos dois primeiros termos presentes:
df['loc_01'] = df['localizacao'].str.split(',').str[0]
df['loc_02'] = df['localizacao'].str.split(',').str[1]
# Mapeamento com dicionário para a criação da variável "estado":
df['estado'] = df['loc_01'].map(dicionario_brasil.dic)
df['estado_02'] = df['loc_02'].map(dicionario_brasil.dic)
df['estado'] = df['estado'].fillna(df['estado_02'])
In [49]:
# Verificação da quantidade de localizações estaduais obtidas:
tweets_localizados = sum(df['estado'].value_counts())
pct_tweets_localizados = round((tweets_localizados*100)/len(df),2)
# Visualização dos resultados:
print("Tweets localizados: ", tweets_localizados)
print("Percentual de tweets localizados: ", pct_tweets_localizados,"%")
Tweets localizados:  84071
Percentual de tweets localizados:  46.68 %
In [50]:
# Dataframe com a contagem dos estados:
tweets_estados = df['estado'].value_counts().to_frame().reset_index()
tweets_estados.columns = ['estado', 'qtd_tweets']
# Separação e exibição dos 5 mais presentes:
top5_estados = tweets_estados.head()
top5_estados
Out[50]:
estado qtd_tweets
0 RJ 40141
1 RS 7093
2 SC 5073
3 MG 4497
4 SP 4295
In [51]:
# Gráfico de contagem de palavras relevantes:
fig = go.Figure(data = [go.Bar(x = top5_estados['estado'], 
                               y = top5_estados['qtd_tweets'], marker_color = 'gold')])
fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Top 5 estados",xref = "paper", x = 0.5))
fig.show()

Taxas de tweets por usuário, por minuto e por segundo:

Para concluir nossa análise exploratória vamos encontrar algumas valores a respeito do nosso período de coleta:

In [52]:
# Calculando o tempo de coleta:
# Criação de um dataframe para a análise:
data_hora_coleta = pd.DataFrame()
# Separação de data e hora:
data_hora_coleta['data'] = [d.date() for d in df['data_hora']]
data_hora_coleta['hora'] = [d.time() for d in df['data_hora']]
# Coletando o último e o primeiro horário:
fim = data_hora_coleta['hora'].iloc[-1] 
inicio = data_hora_coleta['hora'].iloc[0]
# Transformação dos dados:
horario_fim = datetime.datetime.combine(datetime.date.today(), fim)
horario_inicio = datetime.datetime.combine(datetime.date.today(), inicio)
# Cálculo do período de coleta:
periodo = horario_fim - horario_inicio
# Visualização do resultado:
print("Período de coleta: ", periodo)
Período de coleta:  2:59:59
In [53]:
# Taxa média de tweets por usuário:
if qtd_usuarios_unicos > 0:
    media_tweets_por_usuario = round((total_tweets/ qtd_usuarios_unicos),2)
else:
    media_tweets_por_usuario = 0
# Visualização do resultado:    
print("Taxa média de tweets por usuário: ", media_tweets_por_usuario)
Taxa média de tweets por usuário:  1.82
In [54]:
# Taxa média de tweets por usuário verificado:
if qtd_usuarios_verificados > 0:
    media_tweets_por_usuario_verificado = round((total_tt_verificados/ qtd_usuarios_verificados),2)
else:
    media_tweets_por_usuario_verificado = 0
# Visualização do resultado:    
print("Taxa média de tweets por usuário verificado: ", media_tweets_por_usuario_verificado)
Taxa média de tweets por usuário verificado:  3.44
In [55]:
# Taxa média de tweets por usuário não verificado:
if qtd_usuarios_nao_verificados > 0:
    media_tweets_por_usuario_nao_verificado = round((total_tt_nao_verificados/ qtd_usuarios_nao_verificados),2)
else:
    media_tweets_por_usuario_nao_verificado = 0
# Visualização do resultado:    
print("Taxa média de tweets por usuário verificado: ", media_tweets_por_usuario_nao_verificado)
Taxa média de tweets por usuário verificado:  1.82
In [56]:
# Taxa média de tweets por minuto:
tweets_por_minuto = round((total_tweets/(periodo.seconds/60)),2)
# Visualização do resultado: 
print("Taxa média de tweets por minuto: ", tweets_por_minuto)
Taxa média de tweets por minuto:  1000.73
In [57]:
# Taxa média de tweets por segundo:
tweets_por_segundo = round(tweets_por_minuto/60,2)
# Visualização do resultado: 
print("Taxa média de tweets por segundo: ", tweets_por_segundo)
Taxa média de tweets por segundo:  16.68
In [58]:
# Visualização de tabela de taxas com plotly
values_tx = [['','','','Tweets','Por usuário', 'Por usuário verificado', 
           'Por usuário não verificado', 'Por minuto','Por segundo'],
          ['','','','Taxa média',media_tweets_por_usuario, media_tweets_por_usuario_verificado, 
           media_tweets_por_usuario_nao_verificado, tweets_por_minuto, tweets_por_segundo ]]

palavras_chaves = str(keywords).strip('[]')

fig = go.Figure(data=[go.Table(
  columnorder = [1,2],
  columnwidth = [5,5],
  header = dict(values = [['Tweets'],['Taxa média']], line_color = 'rgb(122, 94, 8)', fill_color = 'black',
                align = ['center','center'], font = dict(color = 'gold', size = 11), height = 25),
  cells = dict(values = values_tx, line_color = [['rgb(122, 94, 8)'if (val != '') else 'black' for val in values_tx[0]],
                ['rgb(122, 94, 8)'if (val != '') else 'black' for val in values_tx[1]]],fill_color = 'black', 
                align = ['center', 'center'], font = dict(color = [['gold'if (val == 'Tweets') else
                'white' for val in values_tx[0]], ['gold'if (val == 'Taxa média') else 
                'white' for val in values_tx[1]]], size = 11), height = 25))])

fig.add_trace(go.Table(header=dict(values=['Termos monitorados'],line_color='rgb(122, 94, 8)',fill_color='black',
                align=['center','center'],font=dict(color='gold', size=12),height=30),
                cells=dict(values=[[palavras_chaves,]],line_color='rgb(122, 94, 8)',
                fill=dict(color=['black', 'black']), align=['center', 'center'],font_size=12,height=30)))

fig.update_layout(template = "plotly_dark", 
                  title = go.layout.Title(text = "Taxas de tweets",xref = "paper", x = 0.5))
fig.show()

Apresentação de resultados:

Por fim, vamos condensar toda as análises feitas em um dashboard que nos permita ver as principais informações em uma única página. Podemos usar o próprio jupyter notebook para prototipar nosso dashboard antes de fazê-lo em um arquivo .py, basta fazer alguns ajustes nos códigos utilizados para fazer os gráficos durante a análise exploratória.

In [59]:
# Dashboard
# Estrutura:
fig = make_subplots(
    rows=3, cols=4, start_cell="top-left",
    specs=[[{"colspan": 2},None, {"type": "domain"}, {"type": "domain"}],
           [{"colspan": 2},None, {},{"rowspan": 2, "type": "table"}], [{},{},{},None]],
    subplot_titles=("Tweets ao longo do período", total_tt_st, total_us_st, "Sentimento ao longo do período",
                    "Polaridade","","Top 5 hashtags", "Top 5 palavras", "Top 5 estados"))

# Tweets ao longo do período:
fig.add_trace(go.Scatter(x=tw_x_pd['data_hora'], y=tw_x_pd['qtd_tweets'], fill='tozeroy', mode= 'lines',
                        line=dict(color='gold', width=1),name=""),row=1, col=1)
# Quantidade de tweets:
fig.add_trace(go.Pie(labels = ['Originais', 'Retweets'], values = [originais, retweets], hole = .7,
                     marker_colors=['gold', 'darkgoldenrod'], textinfo='label+percent', 
                     hoverinfo='value'),row=1, col=3)
# Quantidade de usúarios:
fig.add_trace(go.Pie(labels = ['Verificados', 'Não verificados'],
                    values = [qtd_usuarios_verificados, qtd_usuarios_nao_verificados],
                    hole = .5, marker_colors=['darkgoldenrod','gold'], textinfo='label+percent',
                    hoverinfo='value',rotation=90),row=1, col=4)
# Sentimento ao longo do período:
fig.add_trace(go.Scatter(x=total_sents_pos['data_hora'], y=total_sents_pos['qtd_tweets'], line=dict(color='gold', width=1),name="Positivo"),row=2, col=1)
fig.add_trace(go.Scatter(x=total_sents_neu['data_hora'], y=total_sents_neu['qtd_tweets'], line=dict(color='white', width=1),name="Neutro"),row=2, col=1)
fig.add_trace(go.Scatter(x=total_sents_neg['data_hora'], y=total_sents_neg['qtd_tweets'], line=dict(color='darkgoldenrod', width=1),name="Negativo"),row=2, col=1)
# Polaridade:
cores_dic = {'Positivo': 'gold', 'Neutro': 'white', 'Negativo': 'darkgoldenrod'}
cores= polaridade_sentimentos['sentimento'].map(cores_dic)
fig.add_trace(go.Bar( x=polaridade_sentimentos['num_de_tweets'],y=polaridade_sentimentos['sentimento'],orientation='h',
                     marker_color = cores, name=""),row=2, col=3).update_yaxes(categoryorder="total ascending")
# Tabela de taxas:
values_tx = [['','','','Tweets','Por usuário', 'Por usuário verificado','Por usuário não verificado', 
              'Por minuto', 'Por segundo'],
             ['','','','Taxa média',media_tweets_por_usuario, media_tweets_por_usuario_verificado, 
           media_tweets_por_usuario_nao_verificado, tweets_por_minuto, tweets_por_segundo ]]
palavras_chaves = str(keywords).strip('[]')
fig.add_trace(go.Table(columnorder = [1,2],columnwidth = [5,5],
  header = dict( values = [['Tweets'],['Taxa média']],line_color='rgb(122, 94, 8)', fill_color='black',
    align=['center','center'], font=dict(color='gold', size=11),height=25),
  cells=dict(values=values_tx,
    line_color=[['rgb(122, 94, 8)'if (val != '') else 'rgb(17, 17, 17)' for val in values_tx[0]],
               ['rgb(122, 94, 8)'if (val != '') else 'rgb(17, 17, 17)' for val in values_tx[1]]],
    fill_color='rgb(17, 17, 17)',align=['center', 'center'],
    font = dict(color =[['gold'if (val == 'Tweets') else 'white' for val in values_tx[0]],
               ['gold'if (val == 'Taxa média') else 'white' for val in values_tx[1]]], size = 11),
    height=25)),row=2, col=4)
fig.add_trace(go.Table(header=dict(values=['Termos monitorados'],line_color='rgb(122, 94, 8)',fill_color='rgb(17, 17, 17)',
                            align=['center','center'],font=dict(color='gold', size=12),height=30),
                 cells=dict(values=[[palavras_chaves,]],line_color='rgb(122, 94, 8)',
                            fill=dict(color=['rgb(17, 17, 17)', 'rgb(17, 17, 17)']),
                            align=['center', 'center'],font_size=12,height=30)),row=2, col=4)
# Top 5 Hashtags:
fig.add_trace(go.Bar(y = top5_hashtags['count'], x = top5_hashtags['palavra'],
                     marker_color='gold',name=""),row=3, col=1)
# Top 5 Palavras:
fig.add_trace(go.Bar(y = top5_contagem_palavras_relevantes['palavra'],
                     x = top5_contagem_palavras_relevantes['count'],
                 marker_color='darkgoldenrod',orientation='h',name=""),row=3, col=2)
# Top 5 Estados:
fig.add_trace(go.Bar(x = top5_estados['estado'].head(5), y=top5_estados['qtd_tweets'].head(5),
                 marker_color='gold',name=""),row=3, col=3)
# Configura tema:
fig.update_layout(showlegend=False, title=go.layout.Title(text="Bem-te-vi Dashboard",xref="paper",x=0.5),
                  template="plotly_dark").update_xaxes(showgrid=False).update_yaxes(showgrid=False)
# Exibe dashboard no notebook:
fig.show()
# Cria um arquivo html com o dashboard:
fig.write_html('passo-a-passo_dashboard.html', auto_open=True)

Esse mesmo código do dashboard e todos os outros de cálculos e análises são usados(adaptados) no arquivo dashboard.py para gerar o nosso dashboard de monitoramento.
Pode-se também salvar em formato .csv o dataset atualizado com os dados obtidos nas análises.
Note que excluiremos as colunas criadas para manipulação dos dados, mantendo apenas as originais e as com os valores finais das análises:

In [60]:
# Criação do dataset atualizado com a análise. 
dataset_final = df.drop(['estado_02','emojis_extraidos','analise_sent_emoji', 
                         'localizacao', 'loc_01', 'loc_02'],axis=1)
In [61]:
# Exporta o dataset com as análises em arquivo .csv
# dataset_final.to_csv('dataset_final.csv', index=False)

Com os arquivos executa_todos.py e gera_relatorio.sh, fazemos todas essas etapas de forma automatizada sem necessidade de abrir o jupyter notebook.
O executa_todos.py inicia a coleta e abre um dashboard de monitoramento que é atualizado periodicamente.
Já o gera_relatorio.sh gera um relatório com os gráficos obtidos nas análises, além de um dataset em .csv e um arquivo com o último estado do dashboard.